/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ts, NodePredicate, rule } from 'qvog-dsl';

const SEVERITY = 2;
const DOC_PATH = 'docs/no-cond-assign.md';
const DESCRIPTION = 'Disallow assignment operators in conditional expressions';

export type RuleOption = 'always' | 'except-parens';

const ConditionalNode: NodePredicate<ts.Node> = {
  is(node: ts.Node): node is ts.Node {
    return ts.isIfStatement(node) ||
           ts.isForStatement(node) ||
           ts.isWhileStatement(node) ||
           ts.isDoStatement(node) ||
           ts.isConditionalExpression(node);
  }
};

const ASSIGNMENT_OPERATORS = new Set([
  ts.SyntaxKind.EqualsToken,
  ts.SyntaxKind.PlusEqualsToken,
  ts.SyntaxKind.MinusEqualsToken,
  ts.SyntaxKind.AsteriskEqualsToken,
  ts.SyntaxKind.SlashEqualsToken
]);

export default rule<{ ruleOption: RuleOption }>()
  .match(ConditionalNode)
  .when((node, { props }) => {
    const mode = props.ruleOption ?? 'except-parens';
    const condition = getCondition(node);
    if (!condition) {return false};

    const assignments = findAssignments(condition);
    if (assignments.length === 0) {return false};

    if (mode === 'always') {
      return true;
    }

    return assignments.some(assign => !isParenthesized(assign));
  })
  .report({
        severity: SEVERITY,
        description: DESCRIPTION,
        docPath: DOC_PATH,
    });

function getCondition(node: ts.Node): ts.Expression | undefined {
  if (ts.isIfStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node)) {
    return node.expression;
  } else if (ts.isForStatement(node)) {
    return node.condition;
  } else if (ts.isConditionalExpression(node)) {
    return node.condition;
  }
  return undefined;
}

function findAssignments(node: ts.Node): ts.BinaryExpression[] {
  const result: ts.BinaryExpression[] = [];

  const visit = (n: ts.Node): void => {
    if (ts.isBinaryExpression(n) && ASSIGNMENT_OPERATORS.has(n.operatorToken.kind)) {
      result.push(n);
    }
    ts.forEachChild(n, visit);
  };

  visit(node);
  return result;
}

function isParenthesized(node: ts.Node): boolean {
  let current = node.parent;
  while (current) {
    if (ts.isParenthesizedExpression(current)) {
      return true;
    }
    if (ts.isConditionalExpression(current) ||
        ts.isIfStatement(current) ||
        ts.isForStatement(current) ||
        ts.isWhileStatement(current) ||
        ts.isDoStatement(current)) {
      return false;
    }
    current = current.parent;
  }
  return false;
}

